# Copyright (c) 2017 Presto Labs Pte. Ltd.
# Author: inkyu

import logging
import math

from coin.base.book import BookBuilderBase
from coin.base.book.aggregated_book_builder import BookUpdateSide
from coin.exchange.bitflyer_v1.kr_rest.futures_product import BitflyerFuturesProduct
from coin.exchange.kr_rest.product_holders import get_holder_from_product


class BookEntry(object):
  def __init__(self, price, qty):
    self.price = float(price)
    self.qty = float(qty)


def is_close_to_zero(value, abs_tol=1e-15):
  return math.isclose(value, 0., abs_tol=1e-15)


class BookBuilder(BookBuilderBase):
  ProductType = BitflyerFuturesProduct

  def __init__(self, native_symbol, book_builder_observer):
    super().__init__("Bitflyer", native_symbol, book_builder_observer=book_builder_observer)
    self._product_info = get_holder_from_product(self.product).product_info
    logging.info('%s price_precision=%d qty_precision=%d',
                 self.product,
                 self._product_info.price_precision,
                 self._product_info.qty_precision)

  def _sanitize_price_qty(self, price, qty):
    price_mult = 10**self._product_info.price_precision
    qty_mult = 10**self._product_info.qty_precision
    price = round(price * price_mult) / float(price_mult)
    qty = round(qty * qty_mult) / float(qty_mult)
    return price, qty

  def get_diffs(self, bids: list, asks: list):
    diffs = []
    for data in bids:
      price, qty = data["p"], round(data["s"], 4)
      price, qty = self._sanitize_price_qty(float(price), float(qty))
      bid_qty = self._bid_price_map.get(price, BookEntry(price, 0)).qty
      diffs.append([BookUpdateSide.BID, price, round(qty - bid_qty, 4)])

    for data in asks:
      price, qty = data["p"], round(data["s"], 4)
      price, qty = self._sanitize_price_qty(float(price), float(qty))
      ask_qty = self._ask_price_map.get(price, BookEntry(price, 0)).qty
      diffs.append([BookUpdateSide.ASK, price, round(qty - ask_qty, 4)])
    return diffs

  def update(self, bids: list, asks: list):
    for data in bids:
      price, qty = data["p"], data["s"]
      price, qty = self._sanitize_price_qty(float(price), float(qty))
      if is_close_to_zero(price):
        continue
      elif is_close_to_zero(qty):
        self._delete_bid(price)
      else:
        self._insert_bid(price, qty)
    for data in asks:
      price, qty = data["p"], data["s"]
      price, qty = self._sanitize_price_qty(float(price), float(qty))
      if is_close_to_zero(price):
        continue
      elif is_close_to_zero(qty):
        self._delete_ask(price)
      else:
        self._insert_ask(price, qty)

  def _insert_bid(self, price, qty, insert_only=False):
    assert price > 0, price
    assert qty > 0, qty
    entry = BookEntry(price, qty)
    if insert_only:
      assert price not in self._bid_price_map
    self._bid_price_map[price] = entry

  def _insert_ask(self, price, qty, insert_only=False):
    assert price > 0, price
    assert qty > 0, qty
    entry = BookEntry(price, qty)
    if insert_only:
      assert price not in self._ask_price_map
    self._ask_price_map[price] = entry

  def _delete_bid(self, price):
    if price in self._bid_price_map:
      del self._bid_price_map[price]

  def _delete_ask(self, price):
    if price in self._ask_price_map:
      del self._ask_price_map[price]

  def _delete_bid_up_to(self, price):
    elist = self._bid_price_map.get_entries(price, float('Inf'), exclude_max=True)
    for e in list(elist):
      self._delete_bid(e.price)

  def _delete_ask_up_to(self, price):
    elist = self._ask_price_map.get_entries(-float('Inf'), price, exclude_max=True)
    for e in list(elist):
      self._delete_ask(e.price)
